feat(spec): TryCatchErrorValueSchema declares the optional open-string code key the try_catch engine binds - #15672
Conversation
…g code key the try_catch engine binds Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M59rPZZFzqhfMUPFqqZTkf
…ence for the new code key Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M59rPZZFzqhfMUPFqqZTkf
📓 Docs Drift Check2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. What this run could not see
Coarse fallback — 129 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 147c76f0a98a3fc189ede53b2612cf31b3c371d0 && git checkout 147c76f0a98a3fc189ede53b2612cf31b3c371d0
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 58f30d9ef152f07d668bc472c58512cd0c99aeaf 214ae52115974b574704006f4cce19cf064c582e && git checkout -B drift-repro 58f30d9ef152f07d668bc472c58512cd0c99aeaf && git merge --no-ff 214ae52115974b574704006f4cce19cf064c582e
node scripts/docs-audit/affected-docs.mjs --json 58f30d9ef152f07d668bc472c58512cd0c99aeaf |
Fixes #14954
What
packages/spec/src/automation/control-flow.zod.ts—TryCatchErrorValueSchemagains an optionalcode: z.string()key, and thetry_catchconfig'serrorVariabledescribe names it, so the ONE shape the catch region's author, the engine and the run log share again equals what the engine binds. PR #14948 (#14419) added that fifth key on the engine side; the schema was a plainz.objectthat did not declare it, so any round-trip through the declared shape silently STRIPPED it, and the generated reference page documented four keys where the runtime binds five.Typed as an OPEN
string, deliberately notStandardErrorCodeand not the ledger union. The card's warning, quoted from its body: "string, not a closed enum — and this is the part worth not getting wrong. ADR-0112 D3/D4 (with the #9106 amendment) makes the code vocabularyStandardErrorCode ∪ registered ledger codes ∪ tenant-authored codes, andNodeExecutoris third-party-registrable, so a closed type would be a lie the moment anyone registers an executor." The closed-at-every-door rule governsApiErrorSchema.codeat an HTTP door; this value is bound in-process and never crosses one. The docblock on the schema says the same, so the next author is told which decision they are standing on.The describe text, verbatim
code(onTryCatchErrorValue):errorVariable(onTryCatchConfig):Generated followers — regenerated, never hand-edited
Derived, not guessed:
pnpm --filter @objectstack/spec build(runsgen:schema), thenpnpm --filter @objectstack/spec check:generatedto name what is stale, thencheck:generated --fix, which regenerates only what the sweep proved stale.packages/spec/authorable-surface/automation.json— rewritten bygen:schemaduring the build: +1 entry,automation/TryCatchErrorValue:code.content/docs/references/automation/control-flow.mdx— the ONE artifactcheck:generatednamed stale (✗ check:docs, "1 of 15 artifact(s) stale"); regenerated bygen:docsthrough--fix: theerrorVariablerow changes and acoderow joins theTryCatchErrorValuetable.✓):api-surface/(no new export —TryCatchErrorValue (type)/TryCatchErrorValueSchema (const)were already listed),json-schema.manifest/(the schema set is unchanged),export-origins/,declaration-map/,spec-changes.json,docs/protocol-upgrade-guide.md,authorable-surface.base.json(the anchor: it lags by design, was not hand-adjusted, andcheck:authorable-surfaceis green).Strictness-ledger reading
check:strictness-ledgertracks per-file strip-site COUNTS and each row'sClassverdict, not a schema's key set.control-flow.zod.tskeeps exactly one strip site —TryCatchErrorValueSchema, classwire, the "+1 at #13681" row — and an additive optional key moves neither the count nor the class, so the generated counts artifact is unchanged and the gate is green (check:generatedprints✓ check:strictness-ledger; the standalone run exits 0). The hand-written row's key list records what #13681 added and stays true as history; not edited. ADR-0087: an additive optional key retires nothing, so no entry is owed;node scripts/check-adr-0087-registration.mjs --base origin/mainexits 0 on this changeset (that gate judges declared-breaking changesets only).Changeset
.changeset/try-catch-error-value-code-key.md—@objectstack/spec: minor. Semver derivation in one line: a new optional key on a published schema widens the accept set and the exportedTryCatchErrorValuetype without retiring or renaming anything ⇒ minor (not patch — it is a contract widening rather than a behaviour repair; not major — nothing that parsed before stops parsing).Verification — every reading below is on
214ae5211, this PR's head (git rev-parse --short HEAD=214ae5211)Heavy runs went through
scripts/pm/os-verify-lock.sh; verdict lines are quoted from the tools' own output, exit codes captured before any pipe.pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/automation/control-flow.test.ts→Test Files 1 passed (1)·Tests 45 passed (45)(lock VERDICTcommand-exit 0). Four new pins: a round-trip preservescode(asserted as whole-object equality, since a plainz.objectstrips silently);codeis optional and the key is ABSENT, notundefined, when no classified code was carried, and composes withiteration/item; the value is an OPEN string —ACME_RATE_LIMITEDandDUPLICATEparse, whilecode: 42is refused at path['code']withinvalid_type; the describe text reaches the JSON schema (z.toJSONSchema) on both the value anderrorVariable.pnpm --filter @objectstack/spec typecheck→ exit 0 (tsc --noEmit,check:scripts-typecheck,check:test-typecheck: OK);tsc -p tsconfig.test.json --listFilesOnlyreachessrc/automation/control-flow.test.ts(1 hit), so the test layer's verdict covers the edited test file.pnpm turbo run build --filter="@objectstack/service-automation^..."→Tasks: 20 successful, 20 total— thenpnpm --filter @objectstack/service-automation exec vitest run --maxWorkers=2→Test Files 107 passed (107)·Tests 1290 passed (1290)(lock VERDICTcommand-exit 0). That package's vitest config aliases only@objectstack/platform-objectstosrc;@objectstack/specresolves through the workspace link to the REBUILTdist, so the automation:create_recordcollapses the engine'sDUPLICATE_RECORDenvelope to a string, so a flow'stry_catch/faultedge still cannot tell "already there" from "the store is down" #14419 pins increate-record-duplicate-code.test.ts(expect(seen[0]?.code).toBe('DUPLICATE_RECORD'), the loop-row case and the stale-code cases) ran against the new declaration.pnpm --filter @objectstack/service-automation typecheck→ exit 0..d.tsand not a cache: a scratch file underpackages/services/service-automation/srcassigning{ nodeId, message, code: 42 }toTryCatchErrorValue→error TS2322: Type 'number' is not assignable to type 'string'(exit 2, exactly one error), while the positive control on the next line,code: 'DUPLICATE_RECORD', compiled.packages/spec/dist/automation/index.d.ts:1247readscode: z.ZodOptionalwrappingz.ZodString. The scratch file was deleted;git statusis clean.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(no paths; it reported "5 path(s) vs merge base 7087f99 of 'origin/main' and HEAD"): 93 commands, each run on its own with the exit captured before any pipe. 91 exit 0, including every family the dispatch named (check:api-surface,check:authorable-surface,check:browser-reachable-entries,check:dual-source-exports,check:entry-nameability,check:export-origins,check:docs,check:empty-state,check:changeset-gate-self-tests,check:corpus-claim-drift,check:cross-package-test-inputs,check:doc-anchors,check:doc-authoring,check:docs-audit-scope,check:docs-redirects,check:docs-single-h1,check:doc-formula-expressions,check:doc-security-posture) plus, among those the derivation added,check:nul-bytes,check:strictness-ledger,check:generated,check:merge-driver,check:test-source-alias, the changeset trio (check-empty-changeset,check-changeset-no-major,check-adr-0087-registration) andcheck:skill-examples(✅ 257 prose examples type-check across 3 surface(s), after building@objectstack/clientand@objectstack/client-reactso its SDK surface could measure). Two are NOT MEASURED locally, by their own verdict text, and belong to CI:check:dual-build-cjs-loads(exit 3,PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/, 49 unbuilt packages) andcheck:type-check-debt(exit 3,Build the closure first … This is NOT a pass and NOT a finding). One transient, stated rather than hidden:check:docs-audit-scopewent red once (affected-docsself-test, 1 of 568 cases) while an aborted firstcheck:skill-examplesrun had left its extracted-example build files in the tree; it is green on a pristine7087f99ccompare worktree and green on re-run after the cleancheck:skill-examplespass, with 0 ignored.tsfiles remaining underpackages/.pnpm lint(repo-wide eslint) was not run locally and no narrowing measurement is claimed for it — CI owns that run.Hand-written docs
content/docs/automation/flows.mdx:1133already documents{$error.code}(from PR #14948) in terms consistent with the describe text above — "absent otherwise, so a handler branching on it should treat "unset" as "no classified code", not as "nothing failed"" — so nothing there is false; not edited.Out of scope, filed
TryCatchErrorValue & { code?: string }widening intry-catch-node.tsbecomes redundant oncepackages/specdeclarescode(#14954 follow-up) #15669 — the engine's localTryCatchErrorValue & { code?: string }widening attry-catch-node.ts:235becomes redundant once this lands; adomain:servicesfile, so it is a follow-up withBlocked-by: #14954rather than a rider here.🤖 Generated with Claude Code
Generated by Claude Code